package jp.dodododo.dao.object;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import jp.dodododo.dao.exception.IllegalPropertyRuntimeException;
import jp.dodododo.dao.message.Message;
import jp.dodododo.dao.types.JavaType;
import jp.dodododo.dao.util.TypesUtil;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PropertyDesc {

	private static final Log logger = LogFactory.getLog(PropertyDesc.class);

	private String propertyName;

	private Class<?> propertyType;

	private Type genericPropertyType;

	private Method readMethod;

	private Method writeMethod;

	private Field field;

	private ObjectDesc objectDesc;

	private boolean readable = false;

	private boolean writable = false;

	private PropertyDesc(String propertyName, ObjectDesc objectDesc) {
		if (propertyName == null) {
			throw new NullPointerException("propertyName");
		}
		this.propertyName = propertyName;
		this.objectDesc = objectDesc;
	}

	public PropertyDesc(String propertyName, Method readMethod, Method writeMethod, ObjectDesc objectDesc) {
		this(propertyName, objectDesc);
		setReadMethod(readMethod);
		setWriteMethod(writeMethod);
	}

	public PropertyDesc(String propertyName, Field f, ObjectDesc objectDesc) {
		this(propertyName, objectDesc);
		this.field = f;
		setPropertyType(f.getType());
		this.genericPropertyType = f.getGenericType();

		if (Modifier.isPublic(f.getModifiers()) == true) {
			this.readable = true;
			this.writable = true;
		}
	}

	public String getPropertyName() {
		return propertyName;
	}

	public Class<?> getPropertyType() {
		return propertyType;
	}

	public Method getReadMethod() {
		return readMethod;
	}

	public AccessibleObject getReadAccessibleObject() {
		if (isReadable() == false) {
			return null;
		}
		if (readMethod != null) {
			return readMethod;
		} else {
			return field;
		}
	}

	public void setReadMethod(Method readMethod) {
		this.readMethod = readMethod;
		if (readMethod != null) {
			setPropertyType(readMethod.getReturnType());
			this.genericPropertyType = readMethod.getGenericReturnType();
			readable = true;
		}
	}

	public boolean hasReadMethod() {
		return readMethod != null;
	}

	public Method getWriteMethod() {
		return writeMethod;
	}

	public AccessibleObject getWriteAccessibleObject() {
		if (isWritable() == false) {
			return null;
		}
		if (writeMethod != null) {
			return writeMethod;
		} else {
			return field;
		}
	}

	public void setWriteMethod(Method writeMethod) {
		this.writeMethod = writeMethod;
		if (writeMethod != null) {
			setPropertyType(writeMethod.getParameterTypes()[0]);
			this.genericPropertyType = writeMethod.getGenericParameterTypes()[0];
			writable = true;
		}
	}

	public boolean hasWriteMethod() {
		return writeMethod != null;
	}

	public boolean isReadable() {
		return readable;
	}

	public boolean isWritable() {
		return writable;
	}

	public Object getValue(Object target) {
		if (!readable) {
			throw new IllegalStateException(Message.getMessage("00008", propertyName, "readable"));
		}
		try {
			if (readMethod == null) {
				return field.get(target);
			}
			Object value = readMethod.invoke(target, (Object[]) null);
			return value;
		} catch (Throwable t) {
			throw new IllegalPropertyRuntimeException(objectDesc.getTargetClass(), propertyName, t);
		}
	}

	public void setValue(Object target, Object value) {
		if (writable == false) {
			throw new IllegalStateException(Message.getMessage("00008", propertyName, "writable"));
		}
		try {
			Class<?> propertyType = getPropertyType();
			if (propertyType.isPrimitive()) {
				propertyType = toWrapperType(propertyType);
			}
			if (value != null && (propertyType.isInstance(value) == false)) {
				value = convert(value, propertyType);
			}
			if (writeMethod == null) {
				field.set(target, value);
				return;
			}
			writeMethod.invoke(target, new Object[] { value });
		} catch (Throwable t) {
			throw new IllegalPropertyRuntimeException(objectDesc.getTargetClass(), propertyName, value, t);
		}
	}

	@SuppressWarnings("unchecked")
	private static final Map<Class, Class> wrapperTypes = new HashMap<Class, Class>();

	static {
		wrapperTypes.put(boolean.class, Boolean.class);
		wrapperTypes.put(byte.class, Byte.class);
		wrapperTypes.put(short.class, Short.class);
		wrapperTypes.put(int.class, Integer.class);
		wrapperTypes.put(long.class, Long.class);
		wrapperTypes.put(float.class, Float.class);
		wrapperTypes.put(double.class, Double.class);
		wrapperTypes.put(char.class, Character.class);
	}

	@SuppressWarnings("unchecked")
	private Class<?> toWrapperType(Class<?> primitiveType) {
		return wrapperTypes.get(primitiveType);
	}

	private Object convert(Object value, Class<?> dest) {
		if (value == null) {
			return null;
		}
		JavaType<?> javaType = TypesUtil.getJavaType(dest);
		if (javaType == null) {
			throw new UnsupportedOperationException(dest.toString());
		}
		return javaType.convert(value);
	}

	public ObjectDesc getObjectDesc() {
		return objectDesc;
	}

	@Override
	public String toString() {
		return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
	}

	@Override
	public boolean equals(Object o) {
		return EqualsBuilder.reflectionEquals(this, o);
	}

	@Override
	public int hashCode() {
		return HashCodeBuilder.reflectionHashCode(this);
	}

	void setPropertyType(Class<?> propertyType) {
		if (this.propertyType != null && this.propertyType.equals(propertyType) == false) {
			logger.warn(Message.getMessage("00019", objectDesc.getTargetClass().getName(), getPropertyName()));
		}
		this.propertyType = propertyType;
	}

	public Type getGenericPropertyType() {
		return genericPropertyType;
	}
}
